﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: SqlStorageBase.cs
//
//  Description: Provides a base class for SQL storages.
// 
//--------------------------------------------------------------------------

using System;
using System.Reflection;
using System.Data;
using System.Data.Common;
using Chili.Opf3.Query;
using Chili.Opf3.Query.OPathParser;

namespace Chili.Opf3.Storages
{
	/// <summary>
	/// Abstract base class that implements the functionality to access SQL storages.
	/// </summary>
	/// <remarks>
	/// This class implements a abstract base class to access any kind of SQL storage. It
	/// provides the main functionality and some methods that have to be overriden in derived classes.
	/// </remarks>
	/// <example>
	/// For examples how to implement your own storage see: MsSqlStorage, OracleStorage or any other 
	/// storage that comes with the framework.
	/// </example>
	public abstract class SqlStorageBase : StorageBase, ITransactionStorage, IDisposable
	{
		private readonly DateTime _minDateTime = new DateTime(1782, 5, 31);
		private string _connectionString;
		private SqlTransactionBase _transaction;

		/// <summary>
		/// Creates a new instance of the <see cref="SqlStorageBase">SqlStorageBase Class</see>.
		/// </summary>
		/// <param name="connectionString">Specifies a string that contains the information (for ADO.NET) to connect to storage.
		/// </param>
		public SqlStorageBase(string connectionString) : base()
		{
			if (connectionString == null)
				throw new ArgumentNullException("connectionString");
			this.ConnectionString = connectionString;
		}

		#region Overridden Members

		/// <summary>Executes a command on the storage that does not expect any results.</summary>
		/// <remarks>
		/// If the method returns an argument it is used by the framework to set the
		///      <see cref="Opf3.FieldAttribute.AutoNumber">AutoNumber</see> property of the
		///      persistent object.
		/// </remarks>
		/// <returns>An object that represents the value of the %field or null.</returns>
		/// <param name="query">
		/// 	<see cref="Opf3.Query.IQuery">IQuery</see> that is compiled and executed on the
		/// storage.
		/// </param>
		/// <exception cref="ArgumentNullException">Query is null.</exception>
		public override object ExecuteCommand(IQuery query)
		{
			if (query == null)
				throw new ArgumentNullException("query");

			IDbCommand command = null;
			try
			{
				// Compile the query.
				CompiledQuery compiledQuery = query.ToCompiledQuery(this);
				Tracing.Current.TraceStorageIO(compiledQuery);

				// Prepare the command and execute it.
				using (command = PrepareCommand(compiledQuery))
				{
					command.ExecuteNonQuery();
					// If no transaction is active we can close the connection after executing the command.
					if (!IsTransactionActive)
						command.Connection.Close();
				}
				return null;
			}
			catch
			{
				try
				{
					// Cancel the command on an exception.
					if (command != null)
						command.Cancel();
				}
				catch (NotSupportedException)
				{
					// Ignore.
				}

				// Throw the exception.
				throw;
			}
		}

		/// <summary>
		/// Executes the <see cref="Opf3.Query.IQuery">IQuery</see> and returns an 
		/// <see cref="Opf3.ObjectReader{T}">ObjectReader</see> with the results of the query.
		/// </summary>
		/// <param name="context">
		/// 	<see cref="Opf3.ObjectContext">ObjectContext</see> connected with the
		/// <see cref="Opf3.ObjectReader{T}">ObjectReader</see>.
		/// </param>
		/// <param name="query">
		/// 	<see cref="Opf3.Query.IQuery">IQuery</see> that is compiled and executed on the
		/// storage.
		/// </param>
		/// <exception cref="ArgumentNullException">Query is null or context is null.</exception>
		public override ObjectReader<T> ExecuteReader<T>(ObjectContext context, IQuery query)
		{
			if (query == null)
				throw new ArgumentNullException("query");
			if (context == null)
				throw new ArgumentNullException("context");

			IDbCommand command = null;
			try
			{
				// Compile the query.
				CompiledQuery compiledQuery = query.ToCompiledQuery(this);
				Tracing.Current.TraceStorageIO(compiledQuery);

				// Prepare the command for execution.
				command = PrepareCommand(compiledQuery);
				// If a transaction is active we can't close the connection. If no transaction
				// is active the connection is closed when the datareader is closed.
				if (IsTransactionActive)
					return new StorageObjectReader<T>(context, command.ExecuteReader());
				else
					return new StorageObjectReader<T>(context, command.ExecuteReader(CommandBehavior.CloseConnection));
			}
			catch
			{
				try
				{
					// Cancel the command on an exception.
					if (command != null)
						command.Cancel();
				}
				catch (NotSupportedException)
				{
					// Ignore.
				}

				// Throw the exception.
				throw;
			}
		}

		#endregion

		#region ITransactionStorage Members

		/// <summary>
		/// Starts a new transaction.
		/// </summary>
		/// <param name="level"><see cref="System.Data.IsolationLevel">IsolationLevel</see> of the transaction.</param>
		/// <returns>Returns an instance of the <see cref="Transaction">Transaction</see> object.</returns>
		public virtual Transaction StartTransaction(IsolationLevel level)
		{
			lock (this)
			{
				if (_transaction != null)
				{
					return null;
				}

				_transaction = CreateTransaction(level, CreateConnection());
				// Register the events.
				_transaction.TransactionCommit += new EventHandler<EventArgs>(Transaction_TransactionCommit);
				_transaction.TransactionRollback += new EventHandler<EventArgs>(Transaction_TransactionRollback);
			}

			// Fire the event.
			OnTransactionStart(EventArgs.Empty);

			// Log the start of a transaction.
			Tracing.Current.TraceStorageIO(string.Format("Started transaction with IsolationLevel = {0} and ID = {1}.", level.ToString(), _transaction.ID));

			// Return the transaction object.
			return _transaction;
		}

		/// <summary>
		/// Starts a new transaction.
		/// </summary>
		/// <returns>Returns an instance of the <see cref="Transaction">Transaction</see> object.</returns>
		public virtual Transaction StartTransaction()
		{
			return StartTransaction(IsolationLevel.ReadCommitted);
		}

		/// <summary>
		/// Commits the current open transaction on the storage. If no transaction 
		/// is open no exception should be thrown.
		/// </summary>
		public virtual void Commit()
		{
			lock (this)
			{
				if (_transaction != null)
					_transaction.Commit();
			}
		}

		/// <summary>
		/// Rolls the current open transaction back. If no transaction is open no exception
		/// should be thrown.
		/// </summary>
		public virtual void Rollback()
		{
			lock (this)
			{
				if (_transaction != null)
					_transaction.Rollback();
			}
		}

		/// <summary>Returns the current active transaction.</summary>
		/// <value>The current active transaction or null if no transaction is active.</value>
		public virtual Transaction ActiveTransaction
		{
			get { return _transaction; }
			protected set { _transaction = (SqlTransactionBase)value; }
		}

		/// <summary>Returns whether a transaction is currently active.</summary>
		/// <value>True if a transaction is currently open.</value>
		public virtual bool IsTransactionActive
		{
			get { return (_transaction != null); }
		}

		#region Events

		/// <summary>
		/// Event is fired if a transaction has been started.
		/// </summary>
		/// <remarks>This event is fired each time a new transaction is successfully started.</remarks>
		public event EventHandler<EventArgs> TransactionStart;

		/// <summary>
		/// Event is fired if a transaction has been committed.
		/// </summary>
		/// <remarks>This event is fired each time a new transaction is successfully committed.</remarks>
		public event EventHandler<EventArgs> TransactionCommit;

		/// <summary>
		/// Event is fired if a transaction has been aborted.
		/// </summary>
		/// <remarks>
		/// This event is fired each time a new transaction is successfully rolled back.
		/// </remarks>
		public event EventHandler<EventArgs> TransactionRollback;

		/// <summary>
		/// Invoked after a transaction has been started.
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnTransactionStart(EventArgs e)
		{
			if (TransactionStart != null)
				TransactionStart(this, e);
		}

		/// <summary>
		/// Invoked after a transaction has been committed.
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnTransactionCommit(EventArgs e)
		{
			if (TransactionCommit != null)
				TransactionCommit(this, e);
		}
		/// <summary>
		/// Invoked after a transaction has been aborted.
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnTransactionRollback(EventArgs e)
		{
			if (TransactionRollback != null)
				TransactionRollback(this, e);
		}

		#endregion

		#endregion

		/// <summary>
		/// Creates a new transaction.
		/// </summary>
		/// <param name="level"><see cref="System.Data.IsolationLevel">IsolationLevel</see> of the transaction.</param>
		/// <param name="connection">The connection that is used to start the transaction.</param>
		/// <returns>Returns an instance of the <see cref="SqlTransactionBase">SqlTransactionBase</see> object.</returns>
		protected virtual SqlTransactionBase CreateTransaction(IsolationLevel level, IDbConnection connection)
		{
			return new GenericSqlTransaction(connection.BeginTransaction(level));
		}

		/// <summary>
		/// Invoked each time the rollback event of the transaction is fired.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Transaction_TransactionRollback(object sender, EventArgs e)
		{
			_transaction = null;
			// Fire the event.
			OnTransactionRollback(EventArgs.Empty);
		}

		/// <summary>
		/// Invoked each time the commit event of the transaction is fired.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Transaction_TransactionCommit(object sender, EventArgs e)
		{
			_transaction = null;
			// Fire the event.
			OnTransactionCommit(EventArgs.Empty);
		}

		/// <summary>
		/// Routine that prepares the command. Associates the command with a connection and
		/// populates it with data from the <see cref="Opf3.Query.IQuery">IQuery</see>.
		/// </summary>
		/// <param name="query">
		/// 	<see cref="Opf3.Query.IQuery">IQuery</see> that is compiled and executed on the
		/// storage.
		/// </param>
		protected virtual IDbCommand PrepareCommand(CompiledQuery query)
		{
			// Get the transaction connection.
			IDbConnection connection = null;
			if (_transaction != null)
				connection = _transaction.InnerTransaction.Connection;
			else
				connection = CreateConnection();

			// Create a new command object.
			IDbCommand command = query.Command;
			command.CommandTimeout = query.Timeout;

			command.Connection = connection;

			if (_transaction != null)
				command.Transaction = _transaction.InnerTransaction;

			// Throw an exception if the connection is null!
			if (command.Connection == null)
				throw new InvalidOperationException(string.Format("Connection for command is null. Transaction state: {0}", _transaction.InnerTransaction));

			return command;
		}

		/// <summary>
		/// Returns the connection string that specifies to ADO.NET how to connect to the
		/// storage.
		/// </summary>
		/// <value>A string that specifies how to ADO.NET to connect with the storage.</value>
		/// <exception cref="ArgumentNullException">Value is null.</exception>
		public virtual string ConnectionString
		{
			get { return _connectionString; }
			protected set
			{
				if (value == null)
					throw new ArgumentNullException("value");
				_connectionString = value;
			}
		}

		#region Abstract members

		/// <summary>Creates a new storage dependent connection object.</summary>
		/// <returns>Returns an instance of a storage dependent connection object.</returns>
		public abstract IDbConnection CreateConnection();

		/// <summary>Creates a new storage dependent command object.</summary>
		/// <returns>Returns an instance of a storage dependent command object.</returns>
		public abstract IDbCommand CreateCommand();

		/// <summary>Creates and populates a storage dependent parameter with data.</summary>
		/// <returns>
		/// The name of the storage parameter should be set to a valid parameter name (one
		/// that is supported by the storage) while creating the parameter.
		/// </returns>
		/// <param name="index">The index of the parameter.</param>
		/// <param name="name">Name of the storage parameter.</param>
		/// <param name="value">Value of the storage parameter.</param>
		/// <param name="type">The type of the value argument. This information is useful if the value is null.</param>
		/// <exception cref="ArgumentNullException">Name is null.</exception>
		public abstract IDataParameter CreateParameter(int index, ref string name, object value, Type type);

		/// <summary>Transforms the parameter to a valid field.</summary>
		/// <returns>Returns a string that represents a valid field name.</returns>
		/// <remarks>
		/// This method returns a field name that is valid for the current storage. For some
		/// storages it is a better choice to start each field name with a '[' and end with a ']'
		/// (Ms Access) - This method could wrap each field name passed with '[]'.
		/// </remarks>
		/// <param name="name">Name for the field that is transformed.</param>
		public abstract string GetValidFieldName(string name);

		/// <summary>
		/// Transforms the name of the entity to a valid entity name.
		/// </summary>
		/// <returns>Returns a string that represents a valid entity name.</returns>
		/// <remarks>
		/// This method returns an entity name that is valid for the current storage. For
		/// some storages it is a better choice to start each entity name with a '[' and end with a
		/// ']' (Ms Access) - This method could wrap each entity name passed with '[]'.
		/// </remarks>
		/// <param name="name">Name of the entity.</param>
		public abstract string GetValidEntityName(string name);

		#endregion

		#region IDisposable Members

		/// <summary>
		/// Disposes the <see cref="SqlStorageBase">SqlStorageBase</see>.
		/// </summary>
		public void Dispose()
		{
			if (_transaction != null)
				_transaction.Rollback();
		}

		#endregion
	}
}
